# encoding=utf-8
import os
import time
import json
import schedule
import threading
import playsound
from threading import Lock
from datetime import datetime
from collections import OrderedDict
from module import WidgetTip
from data import Global
from utils import Util


class AppController(object):

    def __init__(self, app):
        self.app = app
        self.lock = Lock()

    def todo_timeout(self, todo):
        """
        待办任务定时器回调函数
        添加App.Tips提醒并另起线程执行任务命令
        """
        def run_task_cmd(args=None):
            cmd = DataHandler.get_todo_task_cmd(todo)
            if cmd:
                os.system(cmd)

        def play_tip_sound(args=None):
            playsound.playsound(Global.tip_sound_file)

        if todo not in Global.todo_data:
            return
        self.lock.acquire()
        self.app.add_todo_tip_item(todo, DataHandler.get_todo_text_color(todo))
        Util.create_thread(run_task_cmd)
        if Global.play_sound:
            Util.create_thread(play_tip_sound)
        self.lock.release()

    def load_todo_data(self):
        """
        初始化加载待办数据并更新App界面
        """
        if not DataHandler.load_todo_data():
            return False
        for todo in Global.todo_data:
            self.init_todo_data(todo, data=None)

        self.app.refresh_app_height()
        return True

    @classmethod
    def get_timeout_state(cls, todo, next_run):
        """
        获取倒计时显示字符串以及其前景色
        """
        time_str = '[无]'
        if DataHandler.get_todo_clock_switch(todo):
            time_str = Util.human_time_to_date(next_run) if next_run else '[已过期]'
        return time_str, 'Gray50' if time_str in ('[无]', '[已过期]') else Global.current_theme[2]

    def init_todo_data(self, todo, data):
        """
        新增待办以及初始启动时调用, 用于往App界面添加一项待办事项
        """
        if data and not DataHandler.add_todo_data(todo, data):
            return False

        prio, next_run = DataHandler.init_todo_timer(todo, lambda x=todo: self.todo_timeout(x))
        self.app.add_todo_item(todo, Global.prio_map_fg[prio], self.get_timeout_state(todo, next_run)[0])
        if data:
            self.app.refresh_app_height()
        return True

    def modify_todo_data(self, old_todo, new_todo, data):
        """
        修改待办事项及其数据
        删除旧待办对应的数据及定时器
        新增新待办数据并开启定时器
        """
        if old_todo != new_todo and new_todo in Global.todo_data:
            WidgetTip.error("修改后的待办事项已经存在!")
            return False
        if old_todo not in Global.todo_data:
            WidgetTip.error("%s数据不存在，请确认！" % old_todo)
            return False
        old_todo_data = Global.todo_data[old_todo]
        old_timer_data = Global.todo_timer[old_todo]
        del Global.todo_timer[old_todo]
        del Global.todo_data[old_todo]
        if not DataHandler.add_todo_data(new_todo, data):
            return False
        if new_todo == old_todo and DataHandler.check_is_same_clock(old_todo_data, data):
            Global.todo_timer[new_todo] = old_timer_data
            prio = DataHandler.get_todo_priority(new_todo)
            next_run = DataHandler.get_todo_next_run(new_todo)
        else:
            DateTimer.cancel_timer(old_timer_data[1])
            prio, next_run = DataHandler.init_todo_timer(new_todo, lambda x=new_todo: self.todo_timeout(x))
        time_str, time_color = self.get_timeout_state(new_todo, next_run)
        self.app.update_todo_item(old_todo, time_str, time_color, new_todo, Global.prio_map_fg[prio])
        return True

    def del_todo_data(self, todo):
        """
        删除待办事项及其数据并更新App界面
        """
        DateTimer.cancel_timer(DataHandler.get_todo_timer(todo))
        if DataHandler.del_todo_data(todo):
            self.app.refresh_app_height()

    def update_app_date(self, args=None):
        """
        thread for update datetime in app foot
        """
        while Global.app_running:
            self.app.update_foot_info(date_txt=Util.get_time(week=True))
            time.sleep(1)

    def update_todo_data(self, args=None):
        """
        thread for lazy loading todo data
        and update todo next timeout and app foot info
        时间排序开关打开时，将按照倒计时重新排序待办
        """
        if not self.load_todo_data():
            return

        def assort_todo():
            if rate:
                if next_run:      # 此处的next_run已经经过是否超时校验，不需要再与now()比较
                    time_dict['sche'][todo] = next_run
                else:
                    time_dict['timeout'].append(todo)
            else:
                time_dict['none'].append(todo)
        last_sorted = []
        while Global.app_running:
            today_count, time_dict = 0, OrderedDict({'none': [], 'timeout': [], 'sche': {}})
            for todo in Global.todo_data:
                next_run = DataHandler.get_todo_next_run(todo)
                time_str, time_color = self.get_timeout_state(todo, next_run)
                self.app.update_todo_item(todo, time_str, time_color)
                if Global.time_sort:
                    rate = DataHandler.get_todo_rate(todo)
                    assort_todo()
                if Util.date_is_today(next_run):
                    today_count += 1
            if Global.time_sort:
                sorted_todo = [k[0] for k in sorted(time_dict['sche'].items(), key=lambda x: x[1])] + \
                    time_dict['none'] + time_dict['timeout']
                if last_sorted != sorted_todo:
                    self.sort_todo_item(sorted_todo)
                    last_sorted = sorted_todo
            self.app.update_foot_info(todo_txt="共%s条待办，今日待办%s条." % (len(Global.todo_data), today_count))
            time.sleep(5)

    def sort_todo_item(self, sorted_todo):
        """
        按照倒计时先后重新排序待办
        重新配置todo控件实例与todo事项的对应关系
        """
        old_inst = self.app.fm_body_inst
        if len(sorted_todo) != len(old_inst):
            return
        new_inst = OrderedDict()
        old_keys = list(old_inst.keys())

        for i, todo in enumerate(sorted_todo):
            old_todo = old_keys[i]
            new_inst[todo] = old_inst.get(old_todo)
            if todo != old_todo:
                new_inst[todo][2].configure({'text': todo, 'fg': DataHandler.get_todo_text_color(todo)})
                new_inst[todo][2].update_command(lambda x=todo: self.app.show_todo_detail(x))
                new_inst[todo][1].update_command(lambda x=todo: self.app.del_todo_item(x))
                next_run = DataHandler.get_todo_next_run(todo)
                time_str, time_color = self.get_timeout_state(todo, next_run)
                new_inst[todo][3].configure({'text': time_str, 'fg': time_color})
        self.app.fm_body_inst = new_inst

    @classmethod
    def timer_schedule(cls, args=None):
        """
        thread for run pending schedule jobs
        """
        while Global.app_running:
            time.sleep(1)
            schedule.run_pending()

    def start(self):
        Util.create_thread(self.update_app_date)
        Util.create_thread(self.update_todo_data)
        Util.create_thread(self.timer_schedule)


class DataHandler:

    @classmethod
    def load_todo_data(cls):
        if not os.path.isfile(Global.todo_data_file):
            WidgetTip.error("%s数据文件丢失!" % Global.todo_data_file)
            return False
        try:
            with open(Global.todo_data_file, 'r', encoding='UTF-8') as f:
                Global.todo_data = json.load(f, object_pairs_hook=OrderedDict)
        except:
            WidgetTip.error("%s数据异常!" % Global.todo_data_file)
            return False
        return True

    @classmethod
    def add_todo_data(cls, todo, data):
        Global.todo_data[todo] = data
        try:
            with open(Global.todo_data_file, 'w', encoding='UTF-8') as f:
                json.dump(Global.todo_data, f, ensure_ascii=False, indent=4)
        except:
            WidgetTip.error("更新待办数据失败!")
            return False
        return True

    @classmethod
    def del_todo_data(cls, todo):
        del Global.todo_timer[todo]
        del Global.todo_data[todo]
        try:
            with open(Global.todo_data_file, 'w', encoding='UTF-8') as f:
                json.dump(Global.todo_data, f, ensure_ascii=False, indent=4)
        except:
            WidgetTip.error("更新待办数据失败!")
            return False
        return True

    @classmethod
    def init_todo_timer(cls, todo, callback):
        data = Global.todo_data[todo]
        rate, date_s, time_s, prio = data[1], data[2], data[3], data[6]
        if rate:
            timer = DateTimer.start_timer(rate, date_s.split(), time_s.split(), lambda x=todo: callback(x))
            next_run = DateTimer.get_next_run(rate, date_s.split(), time_s.split(), timer)
        else:
            next_run, timer = None, None
        Global.todo_timer[todo] = (next_run, timer)
        return prio, next_run

    @classmethod
    def check_is_same_clock(cls, old_data, new_data):
        return list(old_data[1:4]) == list(new_data[1:4])

    @classmethod
    def update_todo_next_run(cls, todo):
        data = Global.todo_data.get(todo)
        rate, date_s, time_s = data[1], data[2], data[3]
        if not rate:
            return None
        # timer定时器只有重新编辑了之后才会变化，此处复用timer
        timer = Global.todo_timer[todo][1]
        next_run = DateTimer.get_next_run(rate, date_s.split(), time_s.split(), timer)
        Global.todo_timer[todo] = (next_run, timer)
        return next_run

    @classmethod
    def get_todo_next_run(cls, todo):
        data = Global.todo_timer.get(todo)
        if data:
            next_run = data[0]
            if not next_run:
                return None
            elif next_run > datetime.now():
                return next_run
            else:
                return cls.update_todo_next_run(todo)
        return None

    @classmethod
    def get_todo_text_color(cls, todo):
        data = Global.todo_data.get(todo)
        return Global.prio_map_fg[data[6]] if data else Global.current_theme[2]

    @classmethod
    def get_todo_task_cmd(cls, todo):
        data = Global.todo_data.get(todo)
        return data[5] if data else None

    @classmethod
    def get_todo_priority(cls, todo):
        data = Global.todo_data.get(todo)
        return data[6] if data else None

    @classmethod
    def get_todo_clock_switch(cls, todo):
        data = Global.todo_data.get(todo)
        return data[0] if data else None

    @classmethod
    def get_todo_rate(cls, todo):
        data = Global.todo_data.get(todo)
        return data[1] if data else None

    @classmethod
    def get_todo_timer(cls, todo):
        data = Global.todo_timer.get(todo)
        return data[1] if data else []


class DateTimer:

    @classmethod
    def cancel_timer(cls, timer_list):
        if not timer_list:
            return
        for timer in timer_list:
            if hasattr(timer, 'do'):       # is `schedule.job` timer
                schedule.cancel_job(timer)
            else:                          # is `threading.Timer` timer
                timer.cancel()
                timer.join()
            print('cancel %s' % timer)

    @classmethod
    def start_timer(cls, rate, date_list, time_list, job):
        """
        将闹钟的‘频次’，‘日期’，‘时间’，‘任务’数据转换成任务定时器timer;
        并返回该任务所有定时器的列表

        rate: 按日期 | 每天 | 每周 | 每月
        date_list:
            (按日期): [2022-11-11 ~ 2022-12-10]
            (每天):   [每天]
            (每周):   [星期一 ~ 星期日]
            (每月):   [1号 ~ 31号]
        time_list: [01:00 03:30 12:01 ...]
        job: callback function

        return: timer list to schedule
        """
        timer_list = []
        if rate == '按日期':
            now = datetime.now()
            for date in date_list:
                for time_m in time_list:
                    date_time = datetime.strptime('%s %s' % (date, time_m), "%Y-%m-%d %H:%M")
                    if now > date_time:
                        continue
                    seconds = (date_time - now).total_seconds()
                    timer = threading.Timer(seconds, job)
                    timer.daemon = True
                    timer.start()
                    timer_list.append(timer)
        elif rate == '每天':
            for time_m in time_list:
                timer_list.append(schedule.every().day.at(time_m).do(job))
        elif rate == '每周':
            weekday_timer_map = {
                '星期一': 'schedule.every().monday',
                '星期二': 'schedule.every().tuesday',
                '星期三': 'schedule.every().wednesday',
                '星期四': 'schedule.every().thursday',
                '星期五': 'schedule.every().friday',
                '星期六': 'schedule.every().saturday',
                '星期日': 'schedule.every().sunday'
            }
            for weekday in date_list:
                for time_m in time_list:
                    timer_list.append(eval(weekday_timer_map[weekday]).at(time_m).do(job))
        elif rate == '每月':
            def wrapper_job(days):
                if datetime.today().day not in days:
                    return
                job()
            right_days = [int(day.replace('号', '')) for day in date_list]
            for time_m in time_list:
                timer_list.append(schedule.every().day.at(time_m).do(lambda: wrapper_job(right_days)))
        return timer_list

    @classmethod
    def get_next_run(cls, rate, date_list, time_list, timer_list):
        now = datetime.now()
        next_date = []
        if rate == '按日期':
            for date in date_list:
                for ftime in time_list:
                    date_time = datetime.strptime('%s %s' % (date, ftime), "%Y-%m-%d %H:%M")
                    if now > date_time:
                        continue
                    next_date.append(date_time)
        elif rate in ('每天', '每周'):
            for timer in timer_list:
                next_date.append(timer.next_run)
        elif rate == '每月':
            date_m = ['%s-%s' % (now.year, now.month)]
            if now.month == 11:
                date_m.append('%s-12' % now.year)
            elif now.month == 12:
                date_m.append('%s-1' % (now.year + 1))
            else:
                date_m.append('%s-%s' % (now.year, now.month + 1))
                date_m.append('%s-%s' % (now.year, now.month + 2))
            for date in date_m:
                for day in date_list:
                    for ftime in time_list:
                        try:  # 比如2月没有30 和31号时会异常
                            date_time = datetime.strptime('%s-%s %s' % (date, day.replace('号', ''), ftime),
                                                          "%Y-%m-%d %H:%M")
                            # print(date_time)
                        except:
                            continue
                        if now > date_time:
                            continue
                        # next_minutes.append((date_time - now).total_seconds())
                        next_date.append(date_time)
        return min(next_date) if next_date else None


if __name__ == '__main__':
    def testcase():
        import time
        from utils import Util

        def job():
            print('start job %s' % datetime.now())

        # timer_list = DateTimer.start_timer('按日期', ['2022-11-23'], ['11:38', '11:40'], job)
        # timer_list = DateTimer.start_timer('每天', ['每天'], ['11:38', '15:16'], job)
        timer_list = DateTimer.start_timer('每周', ['星期一', '星期二'], ['11:38', '15:35'], job)
        # timer_list = DateTimer.start_timer('每月', ['20号', '21号'], ['11:38', '20:46'], job)
        print('wait %s' % timer_list)

        # next_run = DateTimer.get_next_run('按日期', ['2022-11-23'], ['11:38'], timer_list)
        # next_run = DateTimer.get_next_run('每天', ['每天'], ['11:38'], timer_list)
        next_run = DateTimer.get_next_run('每周', ['星期一', '星期二'], ['11:38'], timer_list)
        # next_run = DateTimer.get_next_run('每月', ['20号', '21号'], ['11:38'], timer_list)
        minute = int((next_run - datetime.now()).total_seconds() / 60)
        print(Util.human_time(minute) if next_run else '--')

        # time.sleep(5)
        # cancel_timer(timer_list)

        while 1:
            schedule.run_pending()
            time.sleep(1)

    testcase()
